package fr.inserm.u915.tabixserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import net.sf.samtools.tabix.TabixReader;

@SuppressWarnings("serial")
public class SOAPServlet extends HttpServlet
	{
	static private final int LIMIT_ROWS=10000; 
	private int limitRows=LIMIT_ROWS;
	
	private static class Shuttle
		{
		InputStream input;
		XMLEventReader in;
		OutputStream out;
		XMLStreamWriter w;
		Model model;
		Build build;
		
		void writeStartSOAP() throws XMLStreamException
			{
			w.writeStartElement(SOAPEnv.prefix, "Envelope", SOAPEnv.NS);
			w.writeAttribute("xmlns:"+XSI.prefix, XSI.NS);
			w.writeAttribute("xmlns:"+SOAP.prefix, SOAP.NS);
			w.writeAttribute("xmlns:"+SOAPEnv.prefix, SOAPEnv.NS);
			w.writeAttribute("xmlns:"+TNS.prefix, TNS.NS);
			w.writeStartElement(SOAPEnv.prefix, "Body", SOAPEnv.NS);
			}
		void writeEndSOAP() throws XMLStreamException
			{
			w.writeEndElement();
			w.writeEndElement();
			}
		}
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException
		{
		Shuttle param=new Shuttle();
		param.input=req.getInputStream();
		param.out=resp.getOutputStream();
		
		param.model=(Model)this.getServletContext().getAttribute(TabixCtxListener.TABIX_MODEL_ATTRIBUTE);
		if(param.model==null) throw new ServletException("model?");
		param.build=(Build)req.getAttribute("build");
		if(param.build==null) throw new ServletException("build?");
		try {
			XMLInputFactory factory = XMLInputFactory.newInstance();
			factory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.TRUE);
			factory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
			factory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
			factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE);
			param.in=factory.createXMLEventReader(param.input);
			
			resp.setContentType("text/xml");
			XMLOutputFactory xmlfactory= XMLOutputFactory.newInstance();
			param.w= xmlfactory.createXMLStreamWriter(param.out,resp.getCharacterEncoding());
			param.w.writeStartDocument(resp.getCharacterEncoding(), "1.0");
			scanEnvelope(param);
			param.w.writeEndDocument();
			} 
		catch (XMLStreamException e)
			{
			throw new IOException(e);
			}
		finally
			{
			if(param.in!=null)try { param.in.close();} catch(XMLStreamException err) {}
			if(param.input!=null) param.input.close();
			if(param.w!=null)
				{
				try { param.w.flush();param.w.close();}
				catch(XMLStreamException err) {}
				}
			
			if(param.out!=null)
				{
				param.out.flush();
				param.out.close();
				}
			}
		}
	private void scanEnvelope(Shuttle param) throws XMLStreamException,IOException
		{
		while(param.in.hasNext())
			{
			XMLEvent evt=param.in.nextEvent();
			if(!evt.isStartElement()) continue;
			StartElement start=evt.asStartElement();
			if(start.getName().getLocalPart().equals("Envelope") &&
				start.getName().getNamespaceURI().equals(SOAPEnv.NS))
				{
				scanBody(param);
				return;	
				}
			}
		doThrow(param,"No SOAPENV:Envelope was found");
		}
	
	
	private void scanBody(Shuttle param) throws XMLStreamException,IOException
		{
		while(param.in.hasNext())
			{
			XMLEvent evt=param.in.nextEvent();
			if(!evt.isStartElement()) continue;
			StartElement start=evt.asStartElement();
			if(start.getName().getLocalPart().equals("Body") &&
				start.getName().getNamespaceURI().equals(SOAPEnv.NS))
				{
				scanMethod(param);
				return;	
				}
			}
		doThrow(param,"No SOAPENV:Body was found");
		}

	private void scanMethod(Shuttle param) throws XMLStreamException,IOException
		{
		while(param.in.hasNext())
			{
			XMLEvent evt=param.in.nextEvent();
			if(!evt.isStartElement()) continue;
			StartElement start=evt.asStartElement();
			if(!start.getName().getNamespaceURI().equals(TNS.NS)) continue;
			String methodName=start.getName().getLocalPart();
			if(methodName.equals("getTables"))
				{
				doGetTables(param);
				}
			else if(methodName.equals("getTableById"))
				{
				doGetTableById(param);
				}
			else if(methodName.equals("fetch"))
				{
				doFetchOrCount(param,false);
				}
			else if(methodName.equals("count"))
				{
				doFetchOrCount(param,true);
				}
			else
				{
				doThrow(param,"undefined method name:"+methodName);
				}
			break;
			}
		}
	private void doGetTableById(Shuttle param)throws XMLStreamException
		{
		String id=null;
		while(param.in.hasNext())
			{
			XMLEvent evt=param.in.nextEvent();
			if(!evt.isStartElement()) continue;
			StartElement start=evt.asStartElement();
			String child =start.getName().getLocalPart();
			if(child.equals("id"))
				{
				id=param.in.getElementText().trim();
				break;
				}
			}
		Instance instance=null;
		if(id!=null) instance=param.build.getDeepInstanceById(id);
		
		
		param.writeStartSOAP();
		param.w.writeStartElement(TNS.prefix,"getTableByIdResponse",TNS.NS);
		if(instance!=null)
			{
			instance.writeSoap(param.w);
			}
		param.w.writeEndElement();
		param.writeEndSOAP();
		}
	
	
	private void doFetchOrCount(Shuttle param,boolean count) 
			throws XMLStreamException,IOException
		{
		Instance instance=null;
		String chrom=null;
		Integer chromStart=null;
		Integer chromEnd=null;
		Integer limit=null;
		while(param.in.hasNext())
			{
			XMLEvent evt=param.in.nextEvent();
			if(!evt.isStartElement()) continue;
			StartElement start=evt.asStartElement();
			String child =start.getName().getLocalPart();
			if(child.equals("tableid"))
				{
				String s=param.in.getElementText();
				instance=param.build.getDeepInstanceById(s);
				if(instance==null)
					{
					doThrow(param,"undefined table:"+s);
					return;
					}
				
				}
			else if(child.equals("chrom"))
				{
				chrom=param.in.getElementText();
				}
			else if(child.equals("chromStart"))
				{
				try {
					chromStart=Integer.parseInt(param.in.getElementText());
					}
				catch(Exception err)
					{
					doThrow(param,"bar chromStart"+err.getMessage());
					return;
					}
				}
			else if(child.equals("chromEnd"))
				{
				try {
					chromEnd=Integer.parseInt(param.in.getElementText());
					}
				catch(Exception err)
					{
					doThrow(param,"bar chromEnd"+err.getMessage());
					return;
					}
				}
			else if(child.equals("limit"))
				{
				try {
					limit=Integer.parseInt(param.in.getElementText());
					}
				catch(Exception err)
					{
					limit=null;
					}
				}

			}
		if(instance==null)
			{
			doThrow(param,"tableid missing");
			return;
			}
		if(chrom==null)
			{
			doThrow(param,"chrom missing");
			return;
			}
		if(chromStart!=null && chromStart<0)
			{
			doThrow(param,"bad chromStart "+chromStart);
			return;
			}
		if(chromEnd!=null && chromEnd<0)
			{
			doThrow(param,"bad chromEnd "+chromEnd);
			return;
			}
		
		
		if(limit==null || limit>limitRows)
			{
			limit=limitRows;
			}
		

		
		param.writeStartSOAP();
		param.w.writeStartElement(TNS.prefix,
				(count?"countResponse":"fetchResponse")
				,TNS.NS);
		
		Segment segment=new Segment(
				instance.getTable().getChromosomeNaming().normalize(chrom),
				(chromStart==null?0:chromStart),
				(chromEnd==null?Integer.MAX_VALUE-100:chromEnd)//-100 for side effect 
				);
		TabixReader r=new TabixReader(instance.getPath());
		Pattern regex=Pattern.compile(instance.getTable().getPattern());
		Iterator<String> iter=r.query(
				segment.getChromosome(),
				segment.getStart(),
				segment.getEnd()
				);
		int total=0;
		while(iter.hasNext())
			{
			String line=iter.next();
			
			++total;
			if(total>=limit) break;
			if(count) continue;
			
			String tokens[]=regex.split(line);
			if(tokens.length<instance.size())
				{
				continue;
				}
			
			MappedObject mapped=new MappedObject(instance,tokens);
			mapped.writeSOAP(param.w);
			}
		
		r.close();
		
		/* output count */
		if(count)
			{
			param.w.writeStartElement("total");
			param.w.writeCharacters(String.valueOf(total));
			param.w.writeEndElement();
			}
		
		
		param.w.writeEndElement();
		param.writeEndSOAP();
		}

	
	
	private void doGetTables(Shuttle param)throws XMLStreamException
		{
		param.writeStartSOAP();
		param.w.writeStartElement(TNS.prefix,"getTablesResponse",TNS.NS);
		for(Instance instance:param.build.getAllInstances())
			{
			param.w.writeStartElement("tableDef");
			
			param.w.writeStartElement("id");
			param.w.writeCharacters(instance.getId());
			param.w.writeEndElement();
	
			param.w.writeStartElement("label");
			param.w.writeCharacters(instance.getLabel());
			param.w.writeEndElement();
			
			param.w.writeStartElement("description");
			param.w.writeCharacters(instance.getDescription());
			param.w.writeEndElement();
	
			
			param.w.writeEndElement();
			}
		param.w.writeEndElement();
		param.writeEndSOAP();
		}

	
	
	
	private void doThrow(Shuttle param,String message)throws XMLStreamException
		{
		param.writeStartSOAP();
		param.w.writeStartElement(SOAPEnv.prefix, "Fault", SOAPEnv.NS);
		
		//faultcode
		param.w.writeStartElement("faultcode");
		param.w.writeCharacters(SOAPEnv.prefix+":Client");//required by spec
		param.w.writeEndElement();
		
		param.w.writeStartElement( "faultstring");
		param.w.writeCharacters(message==null?"Error":message);
		param.w.writeEndElement();
		
		param.w.writeStartElement("detail");
		param.w.writeStartElement(TNS.prefix, "message", TNS.NS);
		param.w.writeCharacters(message==null?"Error":message);
		param.w.writeEndElement();
		param.w.writeEndElement();
		
		param.w.writeEndElement();
		param.writeEndSOAP();
		}
	}
